﻿/**
 * Copyright (C) 2004-2009 D3Systems, Inc. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110, USA 
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace OMGProtocol
{
    /// <summary>
    /// This is a Light-Weight XML Parser.
    /// It read data from a channel and collect data until data are available in
    /// the channel.
    /// When a message is complete you can retrieve messages invoking the method
    /// getMsgs() and you can invoke the method areThereMsgs() to know if at least
    /// an message is presents.
    /// </summary>
    public class XmlLightWeightParser
    {
        /// <summary>
        /// The maximum size allowed by a single xml stanza.
        /// </summary>
        private static int maxBufferSize = 1048576;

        /// <summary>
        /// Chars that rappresent CDATA section start
        /// </summary>
        protected static char[] CDATA_START = {'<', '!', '[', 'C', 'D', 'A', 'T', 'A', '['};
        
        /// <summary>
        /// Chars that rappresent CDATA section end
        /// </summary>
        protected static char[] CDATA_END = {']', ']', '>'};

        /// <summary>
        /// Buffer with all data retrieved
        /// </summary>
        protected StringBuilder buffer = new StringBuilder();

        // ---- INTERNAL STATUS -------
        /// <summary>
        /// Initial status
        /// </summary>
        protected static int INIT = 0;

        /// <summary>
        /// Status used when the first tag name is retrieved
        /// </summary>
        protected static int HEAD = 2;

        /// <summary>
        /// Status used when robot is inside the xml and it looking for the tag conclusion
        /// </summary>
        protected static int INSIDE = 3;

        /// <summary>
        /// Status used when a '<' is found and try to find the conclusion tag.
        /// </summary>
        protected static int PRETAIL = 4;

        /// <summary>
        /// Status used when the ending tag is equal to the head tag
        /// </summary>
        protected static int TAIL = 5;

        /// <summary>
        /// Status used when robot is inside the main tag and found an '/' to check '/>'.
        /// </summary>
        protected static int VERIFY_CLOSE_TAG = 6;

        /// <summary>
        /// Status used when you are inside a parameter
        /// </summary>
        protected static int INSIDE_PARAM_VALUE = 7;
        
        /// <summary>
        /// Status used when you are inside a cdata section
        /// </summary>
        protected static int INSIDE_CDATA = 8;
        
        /// <summary>
        /// Status used when you are outside a tag/reading text
        /// </summary>
        protected static int OUTSIDE = 9;
        
        /// <summary>
        /// String array containing the statuses of the xml parsing process
        /// </summary>
        String[] sstatus = {"INIT", "", "HEAD", "INSIDE", "PRETAIL", "TAIL", "VERIFY", "INSIDE_PARAM", "INSIDE_CDATA", "OUTSIDE"};
        
        /// <summary>
        /// Current robot status
        /// </summary>
        protected int status = XmlLightWeightParser.INIT;

        /// <summary>
        /// Index to looking for a CDATA section start or end.
        /// </summary>
        protected int cdataOffset = 0;
        
        /// <summary>
        /// Number of chars that machs with the head tag. If the tailCount is equal to
        /// the head length so a close tag is found.
        /// </summary>
        protected int tailCount = 0;
                
        /// <summary>
        /// Indicate the starting point in the buffer for the next message.
        /// </summary>
        protected int startLastMsg = 0;
        
        /// <summary>
        /// Flag used to discover tag in the form <tag />.
        /// </summary>
        protected Boolean insideRootTag = false;
        
        /// <summary>
        /// Object conteining the head tag
        /// </summary>
        protected StringBuilder head = new StringBuilder(5);

        /// <summary>
        /// List with all finished messages found.
        /// </summary>
        protected List<String> msgs = new List<String>();

        /// <summary>
        /// Current XML element depth
        /// </summary>
        private int depth = 0;

        /// <summary>
        /// Indicates that we are inside a child element
        /// </summary>
        protected Boolean insideChildrenTag = false;

        /// <summary>
        /// Event Logger for Debugging Purposes
        /// </summary>
        System.Diagnostics.EventLog logger;

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="logger"></param>
        public XmlLightWeightParser(System.Diagnostics.EventLog logger)
        {
            // Initialize Logger
            this.logger = logger;
        }

        /// <summary>
        /// Checks if there are messages found
        /// </summary>
        /// <returns>true if the parser has found some complete xml message.</returns>
        public Boolean areThereMsgs()
        {
            return (msgs.Count > 0);
        }

        /// <summary>
        /// Get the array of all messages that have been found.
        /// </summary>
        /// <returns>an array with all messages found</returns>
        public String[] getMsgs()
        {
            String[] res = new String[msgs.Count];
            for (int i = 0; i < res.Length; i++)
            {
                res[i] = msgs.ElementAt(i);
            }
            msgs.Clear();
            invalidateBuffer();
            return res;
        }

        /// <summary>
        /// Method use to re-initialize the buffer
        /// </summary>
        protected void invalidateBuffer()
        {
            if (buffer.Length> 0)
            {
                String str = buffer.ToString().Substring(startLastMsg);
                buffer.Remove(0, buffer.Length);
                buffer.Append(str);
                //buffer.trimToSize();
            }
            startLastMsg = 0;
        }

        /// <summary>
        /// Method that add a message to the list and reinit parser.
        /// </summary>
        /// <param name="msg">The String containing the message found.</param>
        protected void foundMsg(String msg)
        {
            // Add message to the complete message list
            if (msg != null)
            {
                msgs.Add(msg);
            }
            // Move the position into the buffer
            status = XmlLightWeightParser.INIT;
            tailCount = 0;
            cdataOffset = 0;
            head.Length = 0;
            insideRootTag = false;
            insideChildrenTag = false;
            depth = 0;
        }

        /// <summary>
        /// Main reading method
        /// </summary>
        /// <param name="data">The binary data containing the incoming packet</param>
        public void read(byte[] data) {
            invalidateBuffer();
            // Check that the buffer is not bigger than 1 Megabyte. For security reasons
            // we will abort parsing when 1 Mega of queued chars was found.
            
            if (buffer.Length > maxBufferSize) {
                throw new Exception("Stopped parsing never ending stanza");
            }
            //CharBuffer charBuffer = encoder.decode(ioBuffer.buf());

            char[] chars = new char[data.Length];

            System.Text.Decoder d = System.Text.Encoding.UTF8.GetDecoder();
            int charLen = d.GetChars(data, 0, data.Length, chars, 0);
            
            //char[] buf = charBuffer.array();
            //int readByte = charBuffer.remaining();

            
            // Just return if nothing was read
            if (charLen == 0)
            {
                return;
            }

            
            // Verify if the last received byte is an incomplete double byte character
            char lastChar = chars[charLen - 1];
            if (lastChar >= 0xfff0)
            {
    //            if (Log.isDebugEnabled()) {
                logger.WriteEntry("Waiting to get complete char: " + chars);
    //            }
                // Rewind the position one place so the last byte stays in the buffer
                // The missing byte should arrive in the next iteration. Once we have both
                // of bytes we will have the correct character
                
                //ioBuffer.position(ioBuffer.position()-1);

                // Decrease the number of bytes read by one
                
                //readByte--;
                    charLen--;

                // Just return if nothing was read
                if (charLen == 0)
                {
                    return;
                }
            }

            buffer.Append(chars, 0, charLen);
            // Do nothing if the buffer only contains white spaces
            if (buffer[0] <= ' ' && buffer[buffer.Length-1] <= ' ') {
                if ("".Equals(buffer.ToString().Trim())) {
                    // Empty the buffer so there is no memory leak
                    buffer.Remove(0, buffer.Length);
                    return;
                }
            }


            // Robot.
            char ch;
            Boolean isHighSurrogate = false;
            for (int i = 0; i < charLen; i++) {

                ch = chars[i];
                if (isHighSurrogate) {
                    if (Char.IsLowSurrogate(ch)) {
                        // Everything is fine. Clean up traces for surrogates
                        isHighSurrogate = false;
                    }
                    else {
                        // Trigger error. Found high surrogate not followed by low surrogate
                        throw new Exception("Found high surrogate not followed by low surrogate");
                    }
                }
                else if (Char.IsHighSurrogate(ch))
                {
                    isHighSurrogate = true;
                }
                else if (Char.IsLowSurrogate(ch))
                {
                    // Trigger error. Found low surrogate char without a preceding high surrogate
                    throw new Exception("Found low surrogate char without a preceding high surrogate");
                }
                if (status == XmlLightWeightParser.TAIL) {
                    // Looking for the close tag
                    if (depth < 1 && ch == head[tailCount]) {
                        tailCount++;
                        if (tailCount == head.Length) {
                            // Close stanza found!
                            // Calculate the correct start,end position of the message into the buffer
                            int end = buffer.Length - charLen + (i + 1);
                            String msg = buffer.ToString().Substring(startLastMsg, end);
                            // Add message to the list
                            foundMsg(msg);
                            startLastMsg = end;
                        }
                    } else {
                        tailCount = 0;
                        status = XmlLightWeightParser.INSIDE;
                    }
                }
                else if (status == XmlLightWeightParser.PRETAIL)
                {
                    if (ch == XmlLightWeightParser.CDATA_START[cdataOffset])
                    {
                        cdataOffset++;
                        if (cdataOffset == XmlLightWeightParser.CDATA_START.Length)
                        {
                            status = XmlLightWeightParser.INSIDE_CDATA;
                            cdataOffset = 0;
                            continue;
                        }
                    } else {
                        cdataOffset = 0;
                        status = XmlLightWeightParser.INSIDE;
                    }
                    if (ch == '/') {
                        status = XmlLightWeightParser.TAIL;
                        depth--;
                    }
                    else if (ch == '!') {
                        // This is a <! (comment) so ignore it
                        status = XmlLightWeightParser.INSIDE;
                    }
                    else {
                        depth++;
                    }
                }
                else if (status == XmlLightWeightParser.VERIFY_CLOSE_TAG)
                {
                    if (ch == '>') {
                        depth--;
                        status = XmlLightWeightParser.OUTSIDE;
                        if (depth < 1) {
                            // Found a tag in the form <tag />
                            int end = buffer.Length - charLen + (i + 1);
                            String msg = buffer.ToString().Substring(startLastMsg, end);
                            // Add message to the list
                            foundMsg(msg);
                            startLastMsg = end;
                        } 
                    } else if (ch == '<') {
                        status = XmlLightWeightParser.PRETAIL;
                        insideChildrenTag = true;
                    } else {
                        status = XmlLightWeightParser.INSIDE;
                    }
                }
                else if (status == XmlLightWeightParser.INSIDE_PARAM_VALUE)
                {
                    
                    if (ch == '"') {
                        status = XmlLightWeightParser.INSIDE;
                    }
                }
                else if (status == XmlLightWeightParser.INSIDE_CDATA)
                {
                    if (ch == XmlLightWeightParser.CDATA_END[cdataOffset])
                    {
                        cdataOffset++;
                        if (cdataOffset == XmlLightWeightParser.CDATA_END.Length)
                        {
                            status = XmlLightWeightParser.OUTSIDE;
                            cdataOffset = 0;
                        }
                    } else {
                        cdataOffset = 0;
                    }
                }
                else if (status == XmlLightWeightParser.INSIDE)
                {
                    if (ch == XmlLightWeightParser.CDATA_START[cdataOffset])
                    {
                        cdataOffset++;
                        if (cdataOffset == XmlLightWeightParser.CDATA_START.Length)
                        {
                            status = XmlLightWeightParser.INSIDE_CDATA;
                            cdataOffset = 0;
                            continue;
                        }
                    } else {
                        cdataOffset = 0;
                        status = XmlLightWeightParser.INSIDE;
                    }
                    if (ch == '"') {
                        status = XmlLightWeightParser.INSIDE_PARAM_VALUE;
                    } else if (ch == '>') {
                        status = XmlLightWeightParser.OUTSIDE;
                        if (insideRootTag && ("stream:stream>".Equals(head.ToString()) ||
                                ("?xml>".Equals(head.ToString())) || ("flash:stream>".Equals(head.ToString())))) {
                            // Found closing stream:stream
                            int end = buffer.Length - charLen + (i + 1);
                            // Skip LF, CR and other "weird" characters that could appear
                            while (startLastMsg < end && '<' != buffer[startLastMsg]) {
                                startLastMsg++;
                            }
                            String msg = buffer.ToString().Substring(startLastMsg, end);
                            foundMsg(msg);
                            startLastMsg = end;
                        }
                        insideRootTag = false;
                    } else if (ch == '/') {
                        status = XmlLightWeightParser.VERIFY_CLOSE_TAG;
                    }
                }
                else if (status == XmlLightWeightParser.HEAD)
                {
                    if (ch == ' ' || ch == '>') {
                        // Append > to head to allow searching </tag>
                        head.Append(">");
                        if(ch == '>')
                            status = XmlLightWeightParser.OUTSIDE;
                        else
                            status = XmlLightWeightParser.INSIDE;
                        insideRootTag = true;
                        insideChildrenTag = false;
                        continue;
                    }
                    else if (ch == '/' && head.Length > 0) {
                        status = XmlLightWeightParser.VERIFY_CLOSE_TAG;
                        depth--;
                    }
                    head.Append(ch);

                }
                else if (status == XmlLightWeightParser.INIT)
                {
                    if (ch == '<') {
                        status = XmlLightWeightParser.HEAD;
                        depth = 1;
                    }
                    else {
                        startLastMsg++;
                    }
                } else if (status == XmlLightWeightParser.OUTSIDE) {
                    if (ch == '<') {
                        status = XmlLightWeightParser.PRETAIL;
                        cdataOffset = 1;
                        insideChildrenTag = true;
                    }
                }
            }
            if (head.Length > 0 &&
                    ("/stream:stream>".Equals(head.ToString()) || ("/flash:stream>".Equals(head.ToString())))) {
                // Found closing stream:stream

                foundMsg("</stream:stream>");
            }
        }
    }
}
